using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using RebelStar.Engine.Graphics;
using RebelStar.Engine.Players.Fog;
using RebelStar.Engine.Sprites;
using Color=Microsoft.Xna.Framework.Graphics.Color;
using Point=Microsoft.Xna.Framework.Point;
using Rectangle=Microsoft.Xna.Framework.Rectangle;

namespace RebelStar.Engine.Maps
{
    public class Map : IEnumerable<MapEntry>
    {
//        private string name;
        private readonly List<TerrainSprite> terrainSprites;
        private readonly List<StructureSprite> structureSprites;
        private readonly List<UnitSprite> unitSprites;
        private readonly List<MapEntry> entries;
        private readonly int width, height;
        private Rectangle visibleRange;
        private bool changed, invalidated;
        private float scale = 1;
        private Size psize;
        private SelectionSprite selection;

        private Map(int width, int height)
        {
            this.entries = new List<MapEntry>(width*height);
            this.width = width;
            this.height = height;
            this.visibleRange = new Rectangle(0, 0, width, height);

            this.unitSprites = new List<UnitSprite>();
            this.terrainSprites = new List<TerrainSprite>();
            this.structureSprites = new List<StructureSprite>();

            this.Invalidate();
        }

#if DEBUG
        public static Map LoadTest()
        {
            Map map = new Map(30, 90);
//            map.name = "Test map";

            map.terrainSprites.Add(new TerrainSprite(@"terrains\plain", 9,9));
            map.terrainSprites.Add(new TerrainSprite(@"terrains\shelf", 9, 9));
            map.terrainSprites.Add(new TerrainSprite(@"terrains\shelf2", 9, 9));
            map.terrainSprites.Add(new TerrainSprite(@"terrains\shelf3", 9, 9));
            map.terrainSprites.Add(new TerrainSprite(@"terrains\water", 9, 9, false));
            map.terrainSprites.Add(new TerrainSprite(@"terrains\water2", 9, 9, false));
            map.terrainSprites.Add(new TerrainSprite(@"terrains\polar", 8, 4));
            map.structureSprites.Add(new StructureSprite(@"terrains\mountains-snow", 4, 4, 88, false, 0));
            map.structureSprites.Add(new StructureSprite(@"terrains\hills", 4, 4, 72, 2, 50));
            map.unitSprites.Add(new UnitSprite(@"units\trooper", 3, 1, 32, 32, 1));
            map.unitSprites.Add(new UnitSprite(@"units\tank", 3, 1, 32, 32, 1));

            map.Selection = new SelectionSprite(@"cursors", 2, 1);

            for (int i = 0; i < map.width*map.height; i++)
            {
                TerrainSprite sprite = map.terrainSprites[0];
                map.entries.Add(new MapEntry(sprite, 0));
            }

            map[map.width-1, map.height-1].ChangeTerrain(map.terrainSprites[4], 0);

//            map[0, 0].AddStructure(structureSprites[0], 0);
//            map[1, 0].AddStructure(structureSprites[0], 1);
//            map[2, 0].AddStructure(structureSprites[0], 2);
//            map[0, 1].AddStructure(structureSprites[0], 3);
//            map[0, 2].AddStructure(structureSprites[0], 4);

//            map[7, 10].AddStructure(structureSprites[0], 1);

//            map[1, 3].SetSelection(map.selection, 1);
//            map[1, 4].SetSelection(map.selection, 2);
//            map[1, 5].SetSelection(map.selection, 3);

//            #region Random generation
//
//            Random r = new Random((int)DateTime.Now.Ticks);
//
//            for (int i = 0; i < map.width*map.height; i++)
//            {
//                SpriteBase sprite = terrainSprites[r.Next(0, terrainSprites.Count)];
//                MapEntry entry = new MapEntry(sprite as TerrainSprite, r.Next(0, sprite.StatesCount));
//                map.entries.Add(entry);
//                int j = r.Next(0, structureSprites.Count * 6);
//                if (j < structureSprites.Count)
//                {
//                    sprite = structureSprites[j];
//                    entry.AddStructure(sprite as StructureSprite, r.Next(0, sprite.StatesCount));
//                }
//            }
//
//            #endregion

            map.changed = true;

            return map;
        }
#endif

        internal void Draw(ISurface surface)
        {
            Draw(surface,
                 visibleRange.X, visibleRange.Y,
                 visibleRange.Width, visibleRange.Height,
                 this.scale);
        }
        internal void Draw(ISurface surface, Dictionary<MapEntry, MapFogEntry> fog)
        {
            Draw(surface,
                 visibleRange.X, visibleRange.Y,
                 visibleRange.Width, visibleRange.Height,
                 this.scale,
                 fog);
        }
        internal void Draw(ISurface surface, int x, int y, int w, int h, float scale)
        {
            this.Draw(surface,
                      x, y,
                      w, h,
                      scale, 
                      null);
        }
        internal void Draw(ISurface surface, int x, int y, int w, int h, float scale, Dictionary<MapEntry, MapFogEntry> fog)
        {
            for (int i = y; i < y+h; i++)
                for (int j = x; j < x+w; j++)
                {
                    if (j >= width || i >= height ||
                        j < 0 || i < 0)
                        continue;

                    PointF c = calculateCoords(j - x, i - y, scale);
                    if (fog != null)
                        MapEntry.BlendColor = fog[this[j, i]].EntryBlendColor;
                    this[j, i].Draw(surface, c.X, c.Y, scale);
                    MapEntry.BlendColor = Color.White;
                }
            invalidated = false;
        }
        internal void DrawSprite(SpriteBase texture, int state, ISurface surface, int x, int y, float scale)
        {
            PointF c = calculateCoords(x-visibleRange.X, y-visibleRange.Y, scale);
            this[x, y].DrawSprite(texture, state, surface, c.X, c.Y, scale);
        }
        public void Scroll(int xofs, int yofs)
        {
            int x = this.visibleRange.X + xofs;
            int y = this.visibleRange.Y + yofs;
            if (x > width - this.visibleRange.Width) x = width - this.visibleRange.Width;
            if (y > Height - this.visibleRange.Height) y = Height - this.visibleRange.Height;
            if (x < 0) x = 0;
            if (y < 0) y = 0;
            this.visibleRange = new Rectangle(
                x,
                y,
                this.visibleRange.Width,
                this.visibleRange.Height);
        }
        public void Resize(Size size)
        {
            Resize(size.Width, size.Height);
        }
        public void Resize(int width, int height)
        {
            psize = new Size(width, height);

            this.visibleRange = new Rectangle(
                this.visibleRange.X,
                this.visibleRange.Y,
                (int) (width/(MapEntry.WIDTH*scale)) + 2,
                (int) (height/(MapEntry.HEIGHT*scale))*2 + 3);
            Invalidate();
        }
        public void Invalidate()
        {
            this.invalidated = true;
        }
        public bool Contains(Point point)
        {
            return point.X >= 0 && point.X < width && point.Y >= 0 && point.Y < height;
        }
        public void SetScale(float scale)
        {
            this.scale = scale;
            this.Resize(psize);
            this.Invalidate();
        }
        internal PointF CalcMapToScreen(Point p)
        {
            return calculateCoords(p.X, p.Y, scale);
        }
        internal Point CalcScreenToMap(Point px)
        {
            return calculateMapEntry(px.X, px.Y, scale);
        }
        internal IList<MapEntry> GetPath(Point p1, Point p2)
        {
            PointF px1 = this.CalcMapToScreen(p1);
            PointF px2 = this.CalcMapToScreen(p2);

            List<MapEntry> entries = new List<MapEntry>();

            int dx = (int) Math.Round(px2.X - px1.X, MidpointRounding.AwayFromZero);
            int dy = (int) Math.Round(px2.Y - px1.Y, MidpointRounding.AwayFromZero);

            Point last = Point.Zero;
            int d = Math.Max(Math.Abs(dx), Math.Abs(dy));
            for (int i = 0; i < d; i++)
            {
                int x = (int)Math.Round(px1.X + i * (dx / (float)d), MidpointRounding.AwayFromZero);
                int y = (int)Math.Round(px1.Y + i * (dy / (float)d), MidpointRounding.AwayFromZero);
                Point p = this.CalcScreenToMap(new Point(
                    (int)(x + (MapEntry.WIDTH * this.scale / 2f)), 
                    (int)(y + (MapEntry.HEIGHT * this.scale / 2f))));

                if (last == p) continue;
                last = p;

                entries.Add(this[p.X - this.VisibleRange.X, p.Y - this.VisibleRange.Y]);
            }
            return entries;
        }
        public Point GetPosition(MapEntry entry)
        {
            for (int i = 0; i < height; i++)
            for (int j = 0; j < width; j++)
                if (this[j,i] == entry)
                    return new Point(j, i);
            return Point.Zero;
        }
        public IEnumerable<MapEntry> GetSelection(Rectangle selectionRect)
        {
            List<MapEntry> sel = new List<MapEntry>();
            if (selectionRect != Rectangle.Empty)
            {
                for (int i = selectionRect.Top; i < selectionRect.Bottom; i++)
                    for (int j = selectionRect.Left; j < selectionRect.Right; j++)
                        sel.Add(this[j, i]);
            }
            return sel;
        }

        public int Width
        {
            get { return width; }
        }
        public int Height
        {
            get { return height; }
        }
        public Rectangle VisibleRange
        {
            get { return visibleRange; }
        }
        public bool Changed
        {
            get { return changed; }
            set { changed = value; }
        }
        public bool Invalidated
        {
            get { return invalidated; }
        }
        internal MapEntry this[Point position]
        { 
            get { return this[position.X, position.Y]; }
        }
        internal MapEntry this[int x, int y]
        {
            get { return entries[y*width + x]; }
        }
        public List<UnitSprite> UnitSprites
        {
            get { return unitSprites; }
        }
        public List<TerrainSprite> TerrainSprites
        {
            get { return terrainSprites; }
        }
        public List<StructureSprite> StructureSprites
        {
            get { return structureSprites; }
        }

        internal SelectionSprite Selection
        {
            get { return selection; }
            private set { selection = value; }
        }

        #region IEnumerable<MapEntry> Members

        public IEnumerator<MapEntry> GetEnumerator()
        {
            return entries.GetEnumerator();
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return entries.GetEnumerator();
        }        
        #endregion

        PointF calculateCoords(int x, int y, float scale)
        {
            return new PointF(
                x*MapEntry.WIDTH*scale - ((visibleRange.Y + y)%2 == 0 ? MapEntry.WIDTH*scale/2f : 0),
                (y*MapEntry.HEIGHT*scale/2f) - (MapEntry.HEIGHT*scale/2f));
        }
        Point calculateMapEntry(int px, int py, float scale)
        {
            int xs = (int)(px / (MapEntry.WIDTH * scale));
            int ys = (int)(py / (MapEntry.HEIGHT * scale));

            int cursorX;
            int cursorY;
            checkSquare(
                px % (MapEntry.WIDTH * scale), 
                py % (MapEntry.HEIGHT * scale), 
                xs, ys * 2 + 1, 
                out cursorX, out cursorY);
            return new Point(cursorX + this.VisibleRange.X, cursorY + this.VisibleRange.Y);
        }
        void checkSquare(float x, float y, int xm, int ym, out int xo, out int yo)
        {
            int Width = (int)(MapEntry.WIDTH * this.scale),
                Height = (int)(MapEntry.HEIGHT * this.scale);

            xo = xm;
            yo = ym;

            float x0, y0;
            float x1, y1;

            x0 = 0;
            y0 = Height / 2;
            x1 = Width / 2;
            y1 = 0;
            if (distToLine(x, y, x0, y0, x1, y1) < 0)
            {
                yo--;
                return;
            }

            x0 = Width / 2;
            y0 = 0;
            x1 = Width;
            y1 = Height / 2;
            if (distToLine(x, y, x0, y0, x1, y1) < 0)
            {
                yo--;
                xo++;
                return;
            }

            x0 = Width;
            y0 = Height / 2;
            x1 = Width / 2;
            y1 = Height;
            if (distToLine(x, y, x0, y0, x1, y1) < 0)
            {
                yo++;
                xo++;
                return;
            }

            x0 = Width / 2;
            y0 = Height;
            x1 = 0;
            y1 = Height / 2;
            if (distToLine(x, y, x0, y0, x1, y1) < 0)
            {
                yo++;
                return;
            }

            return;
        }
        static double distToLine(float x, float y, float x0, float y0, float x1, float y1)
        {
            return ((y0 - y1) * x + (x1 - x0) * y + (x0 * y1 - x1 * y0)) /
                Math.Sqrt(Math.Pow(x1 - x0, 2) + Math.Pow(y1 - y0, 2));
        }
    }
}